/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.core;

import java.io.IOException;
import java.io.InputStream;

import org.springframework.lang.Nullable;
import org.springframework.util.FileCopyUtils;

/**
 * {@code ClassLoader} that does <i>not</i> always delegate to the parent loader
 * as normal class loaders do. This enables, for example, instrumentation to be
 * forced in the overriding ClassLoader, or a "throwaway" class loading behavior
 * where selected application classes are temporarily loaded in the overriding
 * {@code ClassLoader} for introspection purposes before eventually loading an
 * instrumented version of the class in the given parent {@code ClassLoader}.
 *
 * @author Rod Johnson
 * @author Juergen Hoeller
 * @since 2.0.1
 */
public class OverridingClassLoader extends DecoratingClassLoader {

    /**
     * Packages that are excluded by default.
     */
    public static final String[] DEFAULT_EXCLUDED_PACKAGES = new String[]
            {"java.", "javax.", "sun.", "oracle.", "javassist.", "org.aspectj.", "net.sf.cglib."};

    private static final String CLASS_FILE_SUFFIX = ".class";

    static {
        ClassLoader.registerAsParallelCapable();
    }


    @Nullable
    private final ClassLoader overrideDelegate;


    /**
     * Create a new OverridingClassLoader for the given ClassLoader.
     *
     * @param parent the ClassLoader to build an overriding ClassLoader for
     */
    public OverridingClassLoader(@Nullable ClassLoader parent) {
        this(parent, null);
    }

    /**
     * Create a new OverridingClassLoader for the given ClassLoader.
     *
     * @param parent           the ClassLoader to build an overriding ClassLoader for
     * @param overrideDelegate the ClassLoader to delegate to for overriding
     * @since 4.3
     */
    public OverridingClassLoader(@Nullable ClassLoader parent, @Nullable ClassLoader overrideDelegate) {
        super(parent);
        this.overrideDelegate = overrideDelegate;
        for (String packageName : DEFAULT_EXCLUDED_PACKAGES) {
            excludePackage(packageName);
        }
    }


    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if (this.overrideDelegate != null && isEligibleForOverriding(name)) {
            return this.overrideDelegate.loadClass(name);
        }
        return super.loadClass(name);
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        if (isEligibleForOverriding(name)) {
            Class<?> result = loadClassForOverriding(name);
            if (result != null) {
                if (resolve) {
                    resolveClass(result);
                }
                return result;
            }
        }
        return super.loadClass(name, resolve);
    }

    /**
     * Determine whether the specified class is eligible for overriding
     * by this class loader.
     *
     * @param className the class name to check
     * @return whether the specified class is eligible
     * @see #isExcluded
     */
    protected boolean isEligibleForOverriding(String className) {
        return !isExcluded(className);
    }

    /**
     * Load the specified class for overriding purposes in this ClassLoader.
     * <p>The default implementation delegates to {@link #findLoadedClass},
     * {@link #loadBytesForClass} and {@link #defineClass}.
     *
     * @param name the name of the class
     * @return the Class object, or {@code null} if no class defined for that name
     * @throws ClassNotFoundException if the class for the given name couldn't be loaded
     */
    @Nullable
    protected Class<?> loadClassForOverriding(String name) throws ClassNotFoundException {
        Class<?> result = findLoadedClass(name);
        if (result == null) {
            byte[] bytes = loadBytesForClass(name);
            if (bytes != null) {
                result = defineClass(name, bytes, 0, bytes.length);
            }
        }
        return result;
    }

    /**
     * Load the defining bytes for the given class,
     * to be turned into a Class object through a {@link #defineClass} call.
     * <p>The default implementation delegates to {@link #openStreamForClass}
     * and {@link #transformIfNecessary}.
     *
     * @param name the name of the class
     * @return the byte content (with transformers already applied),
     * or {@code null} if no class defined for that name
     * @throws ClassNotFoundException if the class for the given name couldn't be loaded
     */
    @Nullable
    protected byte[] loadBytesForClass(String name) throws ClassNotFoundException {
        InputStream is = openStreamForClass(name);
        if (is == null) {
            return null;
        }
        try {
            // Load the raw bytes.
            byte[] bytes = FileCopyUtils.copyToByteArray(is);
            // Transform if necessary and use the potentially transformed bytes.
            return transformIfNecessary(name, bytes);
        } catch (IOException ex) {
            throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex);
        }
    }

    /**
     * Open an InputStream for the specified class.
     * <p>The default implementation loads a standard class file through
     * the parent ClassLoader's {@code getResourceAsStream} method.
     *
     * @param name the name of the class
     * @return the InputStream containing the byte code for the specified class
     */
    @Nullable
    protected InputStream openStreamForClass(String name) {
        String internalName = name.replace('.', '/') + CLASS_FILE_SUFFIX;
        return getParent().getResourceAsStream(internalName);
    }


    /**
     * Transformation hook to be implemented by subclasses.
     * <p>The default implementation simply returns the given bytes as-is.
     *
     * @param name  the fully-qualified name of the class being transformed
     * @param bytes the raw bytes of the class
     * @return the transformed bytes (never {@code null};
     * same as the input bytes if the transformation produced no changes)
     */
    protected byte[] transformIfNecessary(String name, byte[] bytes) {
        return bytes;
    }

}
